'use strict';

var assert = require('../support/assert');
var _ = require('underscore');
var redis = require('redis');
var step = require('step');
var strftime = require('strftime');
var QueryTables = require('cartodb-query-tables').queryTables;
var NamedMapsCacheEntry = require('../../lib/cache/model/named-maps-entry');
var redisStatsDb = 5;

// Pollute the PG environment to make sure
// configuration settings are always enforced
// See https://github.com/CartoDB/Windshaft-cartodb/issues/174
process.env.PGPORT = '666';
process.env.PGHOST = 'fake';

var path = require('path');
var fs = require('fs');
var http = require('http');

var helper = require('../support/test-helper');

const createServer = require('../../lib/server');
var serverOptions = require('../../lib/server-options');

var LayergroupToken = require('../../lib/models/layergroup-token');

describe('template_api', function () {
    var server;

    before(function () {
        server = createServer(serverOptions);
        server.setMaxListeners(0);
        // FIXME: we need a better way to reset cache while running tests
        server.layergroupAffectedTablesCache.cache.reset();
    });

    var httpRendererResourcesServer;
    before(function (done) {
        // Start a server to test external resources
        httpRendererResourcesServer = http.createServer(function (request, response) {
            var filename = path.join(__dirname, '/../fixtures/http/light_nolabels-1-0-0.png');
            fs.readFile(filename, { encoding: 'binary' }, function (err, file) {
                if (err) {
                    return done();
                }
                response.writeHead(200);
                response.write(file, 'binary');
                response.end();
            });
        });
        httpRendererResourcesServer.listen(8033, done);
    });

    after(function (done) {
        httpRendererResourcesServer.close(done);
    });

    var keysToDelete;
    beforeEach(function () {
        keysToDelete = {};
    });

    afterEach(function (done) {
        helper.deleteRedisKeys(keysToDelete, done);
    });

    var templateAcceptance1 = {
        version: '0.0.1',
        name: 'acceptance1',
        auth: { method: 'open' },
        layergroup: {
            version: '1.0.0',
            layers: [
                {
                    options: {
                        sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, -5e6, 0) as the_geom_webmercator' +
                     ' from test_table limit 2 offset 2',
                        cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
                        cartocss_version: '2.0.2',
                        interactivity: 'cartodb_id'
                    }
                }
            ]
        }
    };

    function makeTemplate (templateName) {
        return {
            version: '0.0.1',
            name: templateName || 'acceptance1',
            auth: { method: 'open' },
            layergroup: {
                version: '1.0.0',
                layers: [
                    {
                        options: {
                            sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, -5e6, 0) as the_geom_webmercator' +
                            ' from test_table limit 2 offset 2',
                            cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
                            cartocss_version: '2.0.2',
                            interactivity: 'cartodb_id'
                        }
                    }
                ]
            }
        };
    }

    function extendDefaultsTemplate (template) {
        return _.extend({}, template, { auth: { method: 'open' }, placeholders: {} });
    }

    it('can add template, returning id', function (done) {
        var expectedTplId = 'acceptance1';
        var postRequest1 = {
            url: '/api/v1/map/named',
            method: 'POST',
            headers: { host: 'localhost', 'Content-Type': 'application/json' },
            data: JSON.stringify(templateAcceptance1)
        };
        step(
            function postUnauthenticated () {
                var next = this;
                assert.response(server, postRequest1, {},
                    function (res) { next(null, res); });
            },
            function postTemplate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 403);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'), res.body);
                err = parsed.errors[0];
                assert.ok(err.match(/only.*authenticated.*user/i),
                    'Unexpected error response: ' + err);
                postRequest1.url += '?api_key=1234';
                var next = this;
                assert.response(server, postRequest1, {},
                    function (res) { next(null, res); });
            },
            function rePostTemplate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsedBody = JSON.parse(res.body);
                var expectedBody = { template_id: expectedTplId };
                assert.deepStrictEqual(parsedBody, expectedBody);

                keysToDelete['map_tpl|localhost'] = 0;

                var next = this;
                assert.response(server, postRequest1, {},
                    function (res) { next(null, res); });
            },
            function checkFailure (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 400, res.body);
                var parsedBody = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsedBody, 'errors'), res.body);
                assert.ok(parsedBody.errors[0].match(/already exists/i),
                    'Unexpected error for pre-existing template name: ' + parsedBody.errors);

                done();
            }
        );
    });

    // See https://github.com/CartoDB/Windshaft-cartodb/issues/128
    it("cannot create template with auth='token' and no valid tokens", function (done) {
        var tplId;
        step(
            function postTemplate1 () {
                // clone the valid one, and give it another name
                var brokenTemplate = JSON.parse(JSON.stringify(templateAcceptance1));
                brokenTemplate.name = 'broken1';
                // Set auth='token' and specify no tokens
                brokenTemplate.auth.method = 'token';
                delete brokenTemplate.auth.tokens;
                var postRequest1 = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(brokenTemplate)
                };
                var next = this;
                assert.response(server, postRequest1, {},
                    function (res) { next(null, res); });
            },
            function checkFailure1 (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 400, res.body);
                var parsedBody = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsedBody, 'errors'), res.body);
                var re = /invalid.*authentication.*missing/i;
                assert.ok(parsedBody.errors[0].match(re),
                    'Error for invalid authentication does not match ' + re + ': ' + parsedBody.errors);
                return null;
            },
            function postTemplate2 (err) {
                assert.ifError(err);
                // clone the valid one and rename it
                var brokenTemplate = JSON.parse(JSON.stringify(templateAcceptance1));
                brokenTemplate.name = 'broken1';
                // Set auth='token' and specify no tokens
                brokenTemplate.auth.method = 'token';
                brokenTemplate.auth.tokens = [];
                var postRequest1 = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(brokenTemplate)
                };
                var next = this;
                assert.response(server, postRequest1, {},
                    function (res) { next(null, res); });
            },
            function checkFailure2 (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 400, res.body);
                var parsedBody = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsedBody, 'errors'), res.body);
                var re = new RegExp(/invalid.*authentication.*missing/i);
                assert.ok(parsedBody.errors[0].match(re),
                    'Error for invalid authentication does not match ' + re + ': ' + parsedBody.errors);
                return null;
            },
            function postTemplateValid (err) {
                assert.ifError(err);
                // clone the valid one and rename it
                var brokenTemplate = JSON.parse(JSON.stringify(templateAcceptance1));
                brokenTemplate.name = 'broken1';
                var postRequest1 = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(brokenTemplate)
                };
                var next = this;
                assert.response(server, postRequest1, {},
                    function (res) { next(null, res); });
            },
            function putTemplateInvalid (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplId = parsed.template_id;
                // clone the valid one and rename it
                var brokenTemplate = JSON.parse(JSON.stringify(templateAcceptance1));
                brokenTemplate.name = 'broken1';
                // Set auth='token' and specify no tokens
                brokenTemplate.auth.method = 'token';
                brokenTemplate.auth.tokens = [];
                var putRequest1 = {
                    url: '/api/v1/map/named/' + tplId + '/?api_key=1234',
                    method: 'PUT',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(brokenTemplate)
                };
                var next = this;
                assert.response(server, putRequest1, {},
                    function (res) { next(null, res); });
            },
            function deleteTemplate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 400, res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'),
                    "Missing 'errors' from response body: " + res.body);
                var re = /invalid.*authentication.*missing/i;
                assert.ok(parsed.errors[0].match(re),
                    'Error for invalid authentication on PUT does not match ' +
            re + ': ' + parsed.errors);
                var delRequest = {
                    url: '/api/v1/map/named/' + tplId + '?api_key=1234',
                    method: 'DELETE',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, delRequest, {},
                    function (res, err) { next(err, res); });
            },
            function checkDelete (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 204, res.statusCode + ': ' + res.body);
                assert.ok(!res.body, 'Unexpected body in DELETE /template response');

                done();
            }
        );
    });

    it('instance endpoint should return CORS headers', function (done) {
        step(function postTemplate1 () {
            var next = this;
            var postRequest = {
                url: '/api/v1/map/named?api_key=1234',
                method: 'POST',
                headers: { host: 'localhost.localhost', 'Content-Type': 'application/json' },
                data: JSON.stringify(templateAcceptance1)
            };
            assert.response(server, postRequest, {}, function (res) { next(null, res); });
        },
        function testCORS () {
            const allowHeaders = 'X-Requested-With, X-Prototype-Version, X-CSRF-Token, Authorization, ' +
                                    'Carto-Event, Carto-Event-Source, Carto-Event-Group-Id, Content-Type';
            assert.response(server, {
                url: '/api/v1/map/named/acceptance1',
                method: 'OPTIONS'
            }, {
                status: 200,
                headers: {
                    'Access-Control-Allow-Headers': allowHeaders,
                    'Access-Control-Allow-Origin': '*'
                }
            }, function () { done(); });
        });
    });

    describe('server-metadata', function () {
        var serverMetadata;
        beforeEach(function () {
            serverMetadata = global.environment.serverMetadata;
            global.environment.serverMetadata = { cdn_url: { http: 'test', https: 'tests' } };
        });

        afterEach(function () {
            global.environment.serverMetadata = serverMetadata;
        });

        it('instance endpoint should return server metadata', function (done) {
            var tmpl = _.clone(templateAcceptance1);
            tmpl.name = 'rambotemplate2';

            step(function postTemplate1 () {
                var next = this;
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(tmpl)
                };
                assert.response(server, postRequest, {}, function (res) {
                    next(null, res);
                });
            },
            function testCORS () {
                var next = this;
                assert.response(server, {
                    url: '/api/v1/map/named/' + tmpl.name,
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' }
                }, {
                    status: 200
                }, function (res) {
                    var parsed = JSON.parse(res.body);
                    keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
                    keysToDelete['user:localhost:mapviews:global'] = 5;
                    assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
                    next(null);
                });
            },
            function deleteTemplate (err) {
                assert.ifError(err);
                var delRequest = {
                    url: '/api/v1/map/named/' + tmpl.name + '?api_key=1234',
                    method: 'DELETE',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' }
                };
                assert.response(server, delRequest, {}, function () {
                    done();
                });
            }
            );
        });
    });

    it('can list templates', function (done) {
        var tplid1, tplid2;
        step(
            function postTemplate1 () {
                var next = this;
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateAcceptance1)
                };
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function postTemplate2 (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplid1 = parsed.template_id;

                var next = this;
                var backupName = templateAcceptance1.name;
                templateAcceptance1.name += '_new';
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateAcceptance1)
                };
                templateAcceptance1.name = backupName;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function litsTemplatesUnauthenticated (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplid2 = parsed.template_id;
                var next = this;
                var getRequest = {
                    url: '/api/v1/map/named',
                    method: 'GET',
                    headers: { host: 'localhost' }
                };
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function litsTemplates (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 403, res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'),
                    'Missing error from response: ' + res.body);
                err = parsed.errors[0];
                assert.ok(err.match(/authenticated user/), err);
                var next = this;
                var getRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'GET',
                    headers: { host: 'localhost' }
                };
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function checkList (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_ids'),
                    "Missing 'template_ids' from response body: " + res.body);
                var ids = parsed.template_ids;
                assert.strictEqual(ids.length, 2);
                assert.ok(ids.indexOf(tplid1) !== -1,
                    'Missing "' + tplid1 + "' from list response: " + ids.join(','));
                assert.ok(ids.indexOf(tplid2) !== -1,
                    'Missing "' + tplid2 + "' from list response: " + ids.join(','));

                keysToDelete['map_tpl|localhost'] = 0;

                done();
            }
        );
    });

    it('can update template', function (done) {
        var tplId;
        step(
            function postTemplate () {
                var next = this;
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(makeTemplate())
                };
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function putMisnamedTemplate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplId = parsed.template_id;
                var backupName = templateAcceptance1.name;
                templateAcceptance1.name = 'changed_name';
                var putRequest = {
                    url: '/api/v1/map/named/' + tplId + '/?api_key=1234',
                    method: 'PUT',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateAcceptance1)
                };
                templateAcceptance1.name = backupName;
                var next = this;
                assert.response(server, putRequest, {},
                    function (res) { next(null, res); });
            },
            function putUnexistentTemplate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 400, res.statusCode + ': ' + res.body);
                var parsedBody = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsedBody, 'errors'), res.body);
                assert.ok(parsedBody.errors[0].match(/cannot update name/i),
                    'Unexpected error for invalid update: ' + parsedBody.errors);
                var putRequest = {
                    url: '/api/v1/map/named/unexistent/?api_key=1234',
                    method: 'PUT',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(makeTemplate())
                };
                var next = this;
                assert.response(server, putRequest, {},
                    function (res) { next(null, res); });
            },
            function putValidTemplate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 400, res.statusCode + ': ' + res.body);
                var parsedBody = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsedBody, 'errors'), res.body);
                assert.ok(parsedBody.errors[0].match(/cannot update name/i),
                    'Unexpected error for invalid update: ' + parsedBody.errors);
                var putRequest = {
                    url: '/api/v1/map/named/' + tplId + '/?api_key=1234',
                    method: 'PUT',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(makeTemplate())
                };
                var next = this;
                assert.response(server, putRequest, {},
                    function (res) { next(null, res); });
            },
            function checkValidUpate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                assert.strictEqual(tplId, parsed.template_id);

                keysToDelete['map_tpl|localhost'] = 0;

                done();
            }
        );
    });

    it('can get a template by id', function (done) {
        var tplId;
        step(
            function postTemplate () {
                var next = this;
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(makeTemplate())
                };
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function getTemplateUnauthorized (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplId = parsed.template_id;
                var getRequest = {
                    url: '/api/v1/map/named/' + tplId,
                    method: 'GET',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function getTemplate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 403, res.statusCode + ': ' + res.body);
                var parsedBody = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsedBody, 'errors'), res.body);
                assert.ok(parsedBody.errors[0].match(/only.*authenticated.*user/i),
                    'Unexpected error for unauthenticated template get: ' + parsedBody.errors);
                var getRequest = {
                    url: '/api/v1/map/named/' + tplId + '?api_key=1234',
                    method: 'GET',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function checkReturnTemplate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template'),
                    "Missing 'template' from response body: " + res.body);
                assert.deepStrictEqual(extendDefaultsTemplate(makeTemplate()), parsed.template);

                keysToDelete['map_tpl|localhost'] = 0;

                done();
            }
        );
    });

    it('can delete a template by id', function (done) {
        var tplId;
        step(
            function postTemplate () {
                var next = this;
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(makeTemplate())
                };
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function getTemplate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplId = parsed.template_id;
                var getRequest = {
                    url: '/api/v1/map/named/' + tplId + '?api_key=1234',
                    method: 'GET',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function deleteTemplateUnauthorized (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template'),
                    "Missing 'template' from response body: " + res.body);
                assert.deepStrictEqual(extendDefaultsTemplate(makeTemplate()), parsed.template);
                var delRequest = {
                    url: '/api/v1/map/named/' + tplId,
                    method: 'DELETE',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, delRequest, {},
                    function (res) { next(null, res); });
            },
            function deleteTemplate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 403, res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'),
                    "Missing 'errors' from response body: " + res.body);
                assert.ok(parsed.errors[0].match(/only.*authenticated.*user/i),
                    'Unexpected error for unauthenticated template get: ' + parsed.errors);
                var delRequest = {
                    url: '/api/v1/map/named/' + tplId + '?api_key=1234',
                    method: 'DELETE',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, delRequest, {},
                    function (res) { next(null, res); });
            },
            function getMissingTemplate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 204, res.statusCode + ': ' + res.body);
                assert.ok(!res.body, 'Unexpected body in DELETE /template response');
                var getRequest = {
                    url: '/api/v1/map/named/' + tplId + '?api_key=1234',
                    method: 'GET',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function checkGetFailure (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 404, res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'),
                    "Missing 'errors' from response body: " + res.body);
                assert.ok(parsed.errors[0].match(/cannot find/i),
                    'Unexpected error for missing template: ' + parsed.errors);

                done();
            }
        );
    });

    it('can instanciate a template by id wadus', function (done) {
        // This map fetches data from a private table
        var templateAcceptance2 = {
            version: '0.0.1',
            name: 'acceptance1',
            auth: { method: 'token', valid_tokens: ['valid1', 'valid2'] },
            layergroup: {
                version: '1.0.0',
                layers: [
                    {
                        options: {
                            sql: 'select * from test_table_private_1 LIMIT 0',
                            cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
                            cartocss_version: '2.0.2',
                            interactivity: 'cartodb_id'
                        }
                    }
                ]
            }
        };

        var templateParams = {};

        var tplId;
        var layergroupid;
        step(
            function postTemplate () {
                var next = this;
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateAcceptance2)
                };
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instanciateNoAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplId = parsed.template_id;
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId,
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateParams)
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            // See https://github.com/CartoDB/Windshaft-cartodb/issues/173
            function instanciateForeignDB (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 403,
                    'Unexpected success instanciating template with no auth: ' + res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'),
                    "Missing 'errors' from response body: " + res.body);
                assert.ok(parsed.errors[0].match(/unauthorized/i),
                    'Unexpected error for unauthorized instance : ' + parsed.errors);
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId + '?auth_token=valid2',
                    method: 'POST',
                    headers: { host: 'foreign', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateParams)
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instanciateAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 404, res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'), "Missing 'errors' from response body: " + res.body);
                assert.ok(parsed.errors[0].match(/not found/i), 'Unexpected error for forbidden instance : ' + parsed.errors);
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId + '?auth_token=valid2',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateParams)
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function fetchTileNoAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200,
                    'Instantiating template: ' + res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'layergroupid'),
                    "Missing 'layergroupid' from response body: " + res.body);
                layergroupid = parsed.layergroupid;
                assert.ok(layergroupid.match(/^localhost@/),
                    'Returned layergroupid does not start with signer name: ' + layergroupid);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'last_updated'),
                    "Missing 'last_updated' from response body: " + res.body);

                keysToDelete['user:localhost:mapviews:global'] = 5;
                keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;

                // TODO: check value of last_updated ?
                var getRequest = {
                    url: '/api/v1/map/' + layergroupid + ':cb0/0/0/0.png',
                    method: 'GET',
                    headers: { host: 'localhost' },
                    encoding: 'binary'
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function fetchTileAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 403,
                    'Fetching tile with no auth: ' + res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'),
                    "Missing 'errors' from response body: " + res.body);
                assert.ok(parsed.errors[0].match(/permission denied/i),
                    'Unexpected error for unauthorized instance (expected /permission denied/): ' + parsed.errors);
                var getRequest = {
                    url: '/api/v1/map/' + layergroupid + '/0/0/0.png?auth_token=valid1',
                    method: 'GET',
                    headers: { host: 'localhost' },
                    encoding: 'binary'
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function checkTile (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200,
                    'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body);
                assert.strictEqual(res.headers['content-type'], 'image/png');
                return null;
            },
            // See https://github.com/CartoDB/Windshaft-cartodb/issues/172
            function fetchTileForeignSignature (err) {
                assert.ifError(err);
                var foreignsigned = layergroupid.replace(/[^@]*@/, 'foreign@');
                var getRequest = {
                    url: '/api/v1/map/' + foreignsigned + '/0/0/0.png?auth_token=valid1',
                    method: 'GET',
                    headers: { host: 'localhost' },
                    encoding: 'binary'
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function checkForeignSignerError (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 403,
                    'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'),
                    "Missing 'errors' from response body: " + res.body);
                assert.ok(parsed.errors[0].match(/cannot use/i),
                    'Unexpected error for unauthorized instance (expected /cannot use/): ' + parsed.errors);
                return null;
            },
            function deleteTemplate (err) {
                assert.ifError(err);
                var delRequest = {
                    url: '/api/v1/map/named/' + tplId + '?api_key=1234',
                    method: 'DELETE',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, delRequest, {},
                    function (res) { next(null, res); });
            },
            function fetchTileDeleted (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 204,
                    'Deleting template: ' + res.statusCode + ':' + res.body);
                var getRequest = {
                    url: '/api/v1/map/' + layergroupid + '/0/0/0.png?auth_token=valid1',
                    method: 'GET',
                    headers: { host: 'localhost' },
                    encoding: 'binary'
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function checkTileAvailable (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, 'Tile should be accessible');
                assert.strictEqual(res.headers['content-type'], 'image/png');

                done();
            }
        );
    });

    it('can instanciate a template with torque layer by id', function (done) {
        // This map fetches data from a private table
        var template = {
            version: '0.0.1',
            name: 'acceptance1',
            auth: { method: 'token', valid_tokens: ['valid1', 'valid2'] },
            layergroup: {
                version: '1.1.0',
                layers: [
                    {
                        type: 'torque',
                        options: {
                            sql: 'select * from test_table_private_1 LIMIT 0',
                            cartocss: 'Map { -torque-frame-count:1; -torque-resolution:1; ' +
                       "-torque-aggregation-function:'count(*)'; -torque-time-attribute:'updated_at'; }"
                        }
                    }
                ]
            }
        };

        var templateParams = {};

        var tplId;
        var layergroupid;
        step(
            function postTemplate () {
                var next = this;
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(template)
                };
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instanciateNoAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplId = parsed.template_id;
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId,
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateParams)
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instanciateAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 403,
                    'Unexpected success instanciating template with no auth: ' + res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'),
                    "Missing 'errors' from response body: " + res.body);
                assert.ok(parsed.errors[0].match(/unauthorized/i),
                    'Unexpected error for unauthorized instance : ' + parsed.errors);
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId + '?auth_token=valid2',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateParams)
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function fetchTileNoAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200,
                    'Instantiating template: ' + res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'layergroupid'),
                    "Missing 'layergroupid' from response body: " + res.body);
                layergroupid = parsed.layergroupid;
                assert.ok(layergroupid.match(/^localhost@/),
                    'Returned layergroupid does not start with signer name: ' + layergroupid);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'last_updated'),
                    "Missing 'last_updated' from response body: " + res.body);

                keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
                keysToDelete['user:localhost:mapviews:global'] = 5;

                // TODO: check value of last_updated ?
                var getRequest = {
                    url: '/api/v1/map/' + layergroupid + ':cb0/0/0/0/0.json.torque',
                    method: 'GET',
                    headers: { host: 'localhost' },
                    encoding: 'binary'
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function fetchTileAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 403,
                    'Fetching tile with no auth: ' + res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'),
                    "Missing 'errors' from response body: " + res.body);
                assert.ok(parsed.errors[0].match(/permission denied/i),
                    'Unexpected error for unauthorized instance (expected /permission denied): ' + parsed.errors);
                var getRequest = {
                    url: '/api/v1/map/' + layergroupid + ':cb1/0/0/0/0.json.torque?auth_token=valid1',
                    method: 'GET',
                    headers: { host: 'localhost' },
                    encoding: 'binary'
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function checkTileFetchOnRestart (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200,
                    'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body);
                assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8');
                var cc = res.headers['x-cache-channel'];
                var expectedCC = 'test_windshaft_cartodb_user_1_db:public.test_table_private_1';
                assert.ok(cc);
                assert.strictEqual(cc, expectedCC);
                // hack simulating restart...
                // FIXME: we need a better way to reset cache while running tests
                server.layergroupAffectedTablesCache.cache.reset(); // need to clean channel cache
                var getRequest = {
                    url: '/api/v1/map/' + layergroupid + ':cb1/0/0/0/1.json.torque?auth_token=valid1',
                    method: 'GET',
                    headers: { host: 'localhost' },
                    encoding: 'binary'
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function checkCacheChannel (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200,
                    'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body);
                assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8');
                var cc = res.headers['x-cache-channel'];
                var expectedCC = 'test_windshaft_cartodb_user_1_db:public.test_table_private_1';
                assert.ok(cc, 'Missing X-Cache-Channel on fetch-after-restart');
                assert.strictEqual(cc, expectedCC);
                return null;
            },
            function deleteTemplate (err) {
                assert.ifError(err);
                var delRequest = {
                    url: '/api/v1/map/named/' + tplId + '?api_key=1234',
                    method: 'DELETE',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, delRequest, {},
                    function (res) { next(null, res); });
            },
            function fetchTileDeleted (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 204,
                    'Deleting template: ' + res.statusCode + ':' + res.body);
                var getRequest = {
                    url: '/api/v1/map/' + layergroupid + ':cb2/0/0/0/0.json.torque?auth_token=valid1',
                    method: 'GET',
                    headers: { host: 'localhost' },
                    encoding: 'binary'
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function checkTorqueTileAvailable (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, 'Torque tile should be accessible');
                assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8');

                done();
            }
        );
    });

    it('can instanciate a template with attribute service by id', function (done) {
        // This map fetches data from a private table
        var template = {
            version: '0.0.1',
            name: 'acceptance1',
            auth: { method: 'token', valid_tokens: ['valid1', 'valid2'] },
            layergroup: {
                version: '1.1.0',
                layers: [
                    {
                        options: {
                            sql: 'select * from test_table_private_1 where cartodb_id in ( 5,6 )',
                            cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
                            cartocss_version: '2.0.2',
                            attributes: { id: 'cartodb_id', columns: ['name', 'address'] }
                        }
                    }
                ]
            }
        };

        var templateParams = {};

        var tplId;
        var layergroupid;
        step(
            function postTemplate () {
                var next = this;
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(template)
                };
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instanciateNoAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplId = parsed.template_id;
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId,
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateParams)
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instanciateAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 403,
                    'Unexpected success instanciating template with no auth: ' + res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'),
                    "Missing 'errors' from response body: " + res.body);
                assert.ok(parsed.errors[0].match(/unauthorized/i),
                    'Unexpected error for unauthorized instance : ' + parsed.errors);
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId + '?auth_token=valid2',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateParams)
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function fetchAttributeNoAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200,
                    'Instantiating template: ' + res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'layergroupid'),
                    "Missing 'layergroupid' from response body: " + res.body);
                layergroupid = parsed.layergroupid;
                assert.ok(layergroupid.match(/^localhost@/),
                    'Returned layergroupid does not start with signer name: ' + layergroupid);
                assert.strictEqual(res.headers['x-layergroup-id'], parsed.layergroupid);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'last_updated'),
                    "Missing 'last_updated' from response body: " + res.body);

                keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
                keysToDelete['user:localhost:mapviews:global'] = 5;

                // TODO: check value of last_updated ?
                var getRequest = {
                    url: '/api/v1/map/' + layergroupid + ':cb0/0/attributes/5',
                    method: 'GET',
                    headers: { host: 'localhost' },
                    encoding: 'binary'
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function fetchAttributeAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 403,
                    'Fetching tile with no auth: ' + res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'errors'),
                    "Missing 'errors' from response body: " + res.body);
                assert.ok(parsed.errors[0].match(/permission denied/i),
                    'Unexpected error for unauthorized getAttributes (expected /permission denied/): ' + parsed.errors);
                var getRequest = {
                    url: '/api/v1/map/' + layergroupid + ':cb1/0/attributes/5?auth_token=valid2',
                    method: 'GET',
                    headers: { host: 'localhost' },
                    encoding: 'binary'
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function checkAttribute (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200,
                    'Unexpected error for authorized getAttributes: ' + res.statusCode + ' -- ' + res.body);
                assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8');
                return null;
            },
            function deleteTemplate (err) {
                assert.ifError(err);
                var delRequest = {
                    url: '/api/v1/map/named/' + tplId + '?api_key=1234',
                    method: 'DELETE',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, delRequest, {},
                    function (res) { next(null, res); });
            },
            function fetchAttrDeleted (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 204,
                    'Deleting template: ' + res.statusCode + ':' + res.body);
                var getRequest = {
                    url: '/api/v1/map/' + layergroupid + ':cb2/0/attributes/5?auth_token=valid2',
                    method: 'GET',
                    headers: { host: 'localhost' },
                    encoding: 'binary'
                };
                var next = this;
                assert.response(server, getRequest, {},
                    function (res) { next(null, res); });
            },
            function checkLayerAttributesAvailable (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, 'Layer attributes should be accessible');
                assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8');

                done();
            }
        );
    });

    it('can instanciate a template by id with open auth', function (done) {
        // This map fetches data from a private table
        var templateAcceptanceOpen = {
            version: '0.0.1',
            name: 'acceptance_open',
            auth: { method: 'open' },
            layergroup: {
                version: '1.0.0',
                layers: [
                    {
                        options: {
                            sql: 'select * from test_table_private_1 LIMIT 0',
                            cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
                            cartocss_version: '2.0.2',
                            interactivity: 'cartodb_id'
                        }
                    }
                ]
            }
        };

        var templateParams = {};

        var tplId;
        step(
            function postTemplate () {
                var next = this;
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateAcceptanceOpen)
                };
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instanciateNoAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplId = parsed.template_id;
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId,
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateParams)
                };
                helper.checkNoCache(res);
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instanciateAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200,
                    'Unexpected success instanciating template with no auth: ' + res.statusCode + ': ' + res.body);

                keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
                keysToDelete['map_tpl|localhost'] = 0;
                keysToDelete['user:localhost:mapviews:global'] = 5;

                done();
            }
        );
    });

    it('can instanciate a template using jsonp', function (done) {
        // This map fetches data from a private table
        var templateAcceptanceOpen = {
            version: '0.0.1',
            name: 'acceptance_open_jsonp',
            auth: { method: 'open' },
            layergroup: {
                version: '1.0.0',
                layers: [
                    {
                        options: {
                            sql: 'select * from test_table_private_1 LIMIT 0',
                            cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
                            cartocss_version: '2.0.2',
                            interactivity: 'cartodb_id'
                        }
                    }
                ]
            }
        };

        var tplId;
        step(
            function postTemplate () {
                var next = this;
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateAcceptanceOpen)
                };
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instanciateNoAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplId = parsed.template_id;
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId + '/jsonp?callback=jsonTest',
                    method: 'GET',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function checkInstanciation (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.statusCode + ': ' + res.body);
                // See https://github.com/CartoDB/Windshaft-cartodb/issues/176
                helper.checkCache(res);
                var expectedSurrogateKey = [
                    new QueryTables.QueryMetadata([{
                        dbname: 'test_windshaft_cartodb_user_1_db',
                        schema_name: 'public',
                        table_name: 'test_table_private_1'
                    }]).key(),
                    new NamedMapsCacheEntry('localhost', templateAcceptanceOpen.name).key()
                ].join(' ');
                helper.checkSurrogateKey(res, expectedSurrogateKey);

                /* eslint-disable no-unused-vars, no-eval */
                function jsonTest (body) {
                    keysToDelete['map_cfg|' + LayergroupToken.parse(body.layergroupid).token] = 0;
                }
                eval(res.body);
                /* eslint-enable */

                keysToDelete['map_tpl|localhost'] = 0;
                keysToDelete['user:localhost:mapviews:global'] = 5;

                return null;
            },
            function finish (err) {
                done(err);
            }
        );
    });

    it('can instanciate a template using jsonp with params', function (done) {
        // This map fetches data from a private table
        var templateAcceptanceOpen = {
            version: '0.0.1',
            name: 'acceptance_open_jsonp_params',
            auth: { method: 'open' },
            placeholders: {
                color: { type: 'css_color', default: 'red' }
            },
            layergroup: {
                version: '1.0.0',
                layers: [
                    {
                        options: {
                            sql: 'select * from test_table_private_1 LIMIT 0',
                            cartocss: '#layer { marker-fill: <%= color %>; marker-allow-overlap:true; }',
                            cartocss_version: '2.0.2',
                            interactivity: 'cartodb_id'
                        }
                    }
                ]
            }
        };

        var tplId;
        step(
            function postTemplate () {
                var next = this;
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateAcceptanceOpen)
                };
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instanciateNoAuth (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplId = parsed.template_id;
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId + '/jsonp?callback=jsonTest&config=' + JSON.stringify({ color: 'blue' }),
                    method: 'GET',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function checkInstanciation (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.statusCode + ': ' + res.body);
                // See https://github.com/CartoDB/Windshaft-cartodb/issues/176
                helper.checkCache(res);
                var expectedSurrogateKey = [
                    new QueryTables.QueryMetadata([{
                        dbname: 'test_windshaft_cartodb_user_1_db',
                        schema_name: 'public',
                        table_name: 'test_table_private_1'
                    }]).key(),
                    new NamedMapsCacheEntry('localhost', templateAcceptanceOpen.name).key()
                ].join(' ');
                helper.checkSurrogateKey(res, expectedSurrogateKey);

                /* eslint-disable no-unused-vars, no-eval */
                function jsonTest (body) {
                    keysToDelete['map_cfg|' + LayergroupToken.parse(body.layergroupid).token] = 0;
                }
                eval(res.body);
                /* eslint-enable */

                keysToDelete['map_tpl|localhost'] = 0;
                keysToDelete['user:localhost:mapviews:global'] = 5;

                done();
            }
        );
    });

    it('template instantiation raises mapviews counter', function (done) {
        var layergroup = {
            stat_tag: 'random_tag',
            version: '1.0.0',
            layers: [
                {
                    options: {
                        sql: 'select 1 as cartodb_id, !pixel_height! as h,' +
                   ' ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator',
                        cartocss: '#layer { polygon-fill:red; }',
                        cartocss_version: '2.0.1'
                    }
                }
            ]
        };
        var template = {
            version: '0.0.1',
            name: 'stat_gathering',
            auth: { method: 'open' },
            layergroup: layergroup
        };
        var statskey = 'user:localhost:mapviews';
        var redisStatsClient = redis.createClient(
            {
                port: global.environment.redis.port,
                host: global.environment.redis.host
            });
        var templateId; // will be set on template post
        var now = strftime('%Y%m%d', new Date());
        var errors = [];
        step(
            function cleanStats () {
                var next = this;
                redisStatsClient.select(redisStatsDb, function (err) {
                    if (err) {
                        next(err);
                    } else {
                        redisStatsClient.del(statskey + ':global', next);
                    }
                });
            },
            function doPostTempate (err) {
                assert.ifError(err);
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(template)
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instantiateTemplate (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                templateId = JSON.parse(res.body).template_id;
                var postRequest = {
                    url: '/api/v1/map/named/' + templateId,
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify({})
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function checkGlobalStats (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200,
                    'Instantiating template: ' + res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'layergroupid'),
                    "Missing 'layergroupid' from response body: " + res.body);
                keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
                redisStatsClient.ZSCORE(statskey + ':global', now, this);
            },
            function checkTagStats (err, val) {
                assert.ifError(err);
                assert.strictEqual(val, '1', 'Expected score of ' + now + ' in ' + statskey + ':global to be 1, got ' + val);
                redisStatsClient.ZSCORE(statskey + ':stat_tag:random_tag', now, this);
            },
            function checkTagStatsValue (err, val) {
                assert.ifError(err);
                assert.equal(val, '1', 'Expected score of ' + now + ' in ' + statskey + ':stat_tag:' + layergroup.stat_tag +
              ' to be 1, got ' + val);
                return null;
            },
            function deleteTemplate (err) {
                assert.ifError(err);
                var delRequest = {
                    url: '/api/v1/map/named/' + templateId + '?api_key=1234',
                    method: 'DELETE',
                    headers: { host: 'localhost' }
                };
                var next = this;
                assert.response(server, delRequest, {},
                    function (res) { next(null, res); });
            },
            function cleanupStats (err, res) {
                if (err) {
                    return done(err);
                }
                assert.strictEqual(res.statusCode, 204, res.statusCode + ': ' + res.body);
                if (err) {
                    errors.push('' + err);
                }

                keysToDelete['user:localhost:mapviews:global'] = 5;
                keysToDelete[statskey + ':stat_tag:' + layergroup.stat_tag] = 5;

                done();
            }
        );
    });

    it('instance map token changes with templates certificate changes', function (done) {
        // This map fetches data from a private table
        var templateAcceptance2 = {
            version: '0.0.1',
            name: 'acceptance2',
            auth: { method: 'token', valid_tokens: ['valid1', 'valid2'] },
            layergroup: {
                version: '1.0.0',
                layers: [
                    {
                        options: {
                            sql: 'select * from test_table_private_1 LIMIT 0',
                            cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
                            cartocss_version: '2.0.2',
                            interactivity: 'cartodb_id'
                        }
                    }
                ]
            }
        };

        var templateParams = {};

        var tplId;
        var layergroupid;
        step(
            function postTemplate () {
                var next = this;
                var postRequest = {
                    url: '/api/v1/map/named?api_key=1234',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateAcceptance2)
                };
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instance1 (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                tplId = parsed.template_id;
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId + '?auth_token=valid2',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateParams)
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res, err) { next(err, res); });
            },
            function checkInstance1 (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200,
                    'Instantiating template: ' + res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'layergroupid'),
                    "Missing 'layergroupid' from response body: " + res.body);
                layergroupid = parsed.layergroupid;
                helper.checkSurrogateKey(res, new NamedMapsCacheEntry('localhost', templateAcceptance2.name).key());

                keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
                return null;
            },
            function updateTemplate (err) {
                assert.ifError(err);
                // clone the valid one and rename it
                var changedTemplate = JSON.parse(JSON.stringify(templateAcceptance2));
                changedTemplate.auth.method = 'open';
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId + '/?api_key=1234',
                    method: 'PUT',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(changedTemplate)
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res) { next(null, res); });
            },
            function instance2 (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200, res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'),
                    "Missing 'template_id' from response body: " + res.body);
                assert.strictEqual(tplId, parsed.template_id);
                var postRequest = {
                    url: '/api/v1/map/named/' + tplId + '?auth_token=valid2',
                    method: 'POST',
                    headers: { host: 'localhost', 'Content-Type': 'application/json' },
                    data: JSON.stringify(templateParams)
                };
                var next = this;
                assert.response(server, postRequest, {},
                    function (res, err) { next(err, res); });
            },
            function checkInstance2 (err, res) {
                assert.ifError(err);
                assert.strictEqual(res.statusCode, 200,
                    'Instantiating template: ' + res.statusCode + ': ' + res.body);
                var parsed = JSON.parse(res.body);
                assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'layergroupid'),
                    "Missing 'layergroupid' from response body: " + res.body);
                assert.ok(layergroupid !== parsed.layergroupid);
                helper.checkSurrogateKey(res, new NamedMapsCacheEntry('localhost', templateAcceptance2.name).key());

                keysToDelete['map_tpl|localhost'] = 0;
                keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
                keysToDelete['user:localhost:mapviews:global'] = 5;

                done();
            }
        );
    });

    it('can use an http layer', function (done) {
        var username = 'localhost';

        var httpTemplateName = 'acceptance_http';
        var httpTemplate = {
            version: '0.0.1',
            name: httpTemplateName,
            layergroup: {
                version: '1.3.0',
                layers: [
                    {
                        type: 'http',
                        options: {
                            urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
                            subdomains: [
                                'a',
                                'b',
                                'c'
                            ]
                        }
                    },
                    {
                        type: 'cartodb',
                        options: {
                            sql: 'select * from test_table_private_1',
                            cartocss: '#layer { marker-fill:blue; }',
                            cartocss_version: '2.0.2',
                            interactivity: 'cartodb_id'
                        }
                    }
                ]
            }
        };

        var templateParams = {};

        var expectedTemplateId = httpTemplateName;
        var layergroupid;
        step(
            function createTemplate () {
                var next = this;
                assert.response(
                    server,
                    {
                        url: '/api/v1/map/named?api_key=1234',
                        method: 'POST',
                        headers: {
                            host: username,
                            'Content-Type': 'application/json'
                        },
                        data: JSON.stringify(httpTemplate)
                    },
                    {
                        status: 200
                    },
                    function (res, err) {
                        next(err, res);
                    }
                );
            },
            function instantiateTemplate (err, res) {
                if (err) {
                    throw err;
                }
                assert.deepStrictEqual(JSON.parse(res.body), { template_id: expectedTemplateId });
                var next = this;
                assert.response(
                    server,
                    {
                        url: '/api/v1/map/named/' + expectedTemplateId,
                        method: 'POST',
                        headers: {
                            host: username,
                            'Content-Type': 'application/json'
                        },
                        data: JSON.stringify(templateParams)
                    },
                    {
                        status: 200
                    },
                    function (res) {
                        next(null, res);
                    }
                );
            },
            function fetchTile (err, res) {
                if (err) {
                    throw err;
                }

                var parsed = JSON.parse(res.body);
                assert.ok(
                    Object.prototype.hasOwnProperty.call(parsed, 'layergroupid'), "Missing 'layergroupid' from response body: " + res.body);
                layergroupid = parsed.layergroupid;

                keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
                keysToDelete['user:localhost:mapviews:global'] = 5;

                var next = this;
                assert.response(
                    server,
                    {
                        url: '/api/v1/map/' + layergroupid + '/all/0/0/0.png',
                        method: 'GET',
                        headers: {
                            host: username
                        },
                        encoding: 'binary'
                    },
                    {
                        status: 200
                    },
                    function (res) {
                        next(null, res);
                    }
                );
            },
            function checkTile (err, res) {
                if (err) {
                    throw err;
                }
                assert.strictEqual(res.headers['content-type'], 'image/png');
                return null;
            },
            function deleteTemplate (err) {
                if (err) {
                    throw err;
                }
                var next = this;
                assert.response(
                    server,
                    {
                        url: '/api/v1/map/named/' + expectedTemplateId + '?api_key=1234',
                        method: 'DELETE',
                        headers: {
                            host: username
                        }
                    },
                    {
                        status: 204
                    },
                    function (res, err) {
                        next(err, res);
                    }
                );
            },
            function finish (err) {
                done(err);
            }
        );
    });

    describe('named map nonexistent tokens', function () {
        var username = 'localhost';
        var templateHash = 'deadbeef';
        var nonexistentToken = 'wadus';

        function request (token) {
            return {
                url: '/api/v1/map/' + token + '/all/0/0/0.png',
                method: 'GET',
                headers: {
                    host: username
                },
                encoding: 'binary'
            };
        }

        var expectedResponse = {
            headers: {
                'Content-Type': 'application/json; charset=utf-8'
            },
            status: 400
        };

        function checkTileFn (done) {
            return function checkTile (res, err) {
                if (err) {
                    return done(err);
                }
                assert.deepStrictEqual(JSON.parse(res.body).errors,
                    ["Invalid or nonexistent map configuration token '" + nonexistentToken + "'"]);

                done();
            };
        }

        it('returns an error for named map nonexistent tokens', function (done) {
            var nonexistentNamedMapToken = username + '@' + templateHash + '@' + nonexistentToken;

            assert.response(
                server,
                request(nonexistentNamedMapToken),
                expectedResponse,
                checkTileFn(done)
            );
        });

        it('returns an error for named map nonexistent tokens without template hash', function (done) {
            var nonexistentNamedMapToken = username + '@' + nonexistentToken;

            assert.response(
                server,
                request(nonexistentNamedMapToken),
                expectedResponse,
                checkTileFn(done)
            );
        });
    });

    var torqueParamsScenarios = [
        {
            templateParams: {},
            expectedTile: [{ x__uint8: 125, y__uint8: 159, vals__uint8: [2], dates__uint16: [0] }]
        },
        {
            templateParams: { namesFilter: "'Hawai'" },
            expectedTile: [{ x__uint8: 125, y__uint8: 159, vals__uint8: [1], dates__uint16: [0] }]
        }
    ];
    torqueParamsScenarios.forEach(function (scenario) {
        it('can instantiate with torque layer and params=' + JSON.stringify(scenario.templateParams), function (done) {
            var torqueParamsTemplate = {
                version: '0.0.1',
                name: 'acceptance_torque_params',
                auth: {
                    method: 'open'
                },
                placeholders: {
                    namesFilter: {
                        type: 'sql_ident',
                        default: "'Hawai', 'El Estocolmo'"
                    }
                },
                layergroup: {
                    version: '1.4.0',
                    layers: [
                        {
                            type: 'torque',
                            options: {
                                sql: 'select * from test_table_private_1 where name in (<%= namesFilter %>)',
                                cartocss: 'Map { -torque-frame-count:1; -torque-resolution:1; ' +
                                "-torque-aggregation-function:'count(*)'; -torque-time-attribute:'cartodb_id'; }",
                                cartocss_version: '2.0.2'
                            }
                        }
                    ]
                }
            };

            var layergroupIdToDelete;
            step(
                function createTemplate () {
                    var next = this;
                    var createTemplateRequest = {
                        url: '/api/v1/map/named?api_key=1234',
                        method: 'POST',
                        headers: {
                            host: 'localhost',
                            'Content-Type': 'application/json'
                        },
                        data: JSON.stringify(torqueParamsTemplate)
                    };
                    assert.response(
                        server,
                        createTemplateRequest,
                        {
                            status: 200
                        },
                        function (res, err) {
                            next(err, res);
                        }
                    );
                },
                function instantiateTemplate (err, res) {
                    assert.ifError(err);
                    var parsed = JSON.parse(res.body);
                    assert.ok(Object.prototype.hasOwnProperty.call(parsed, 'template_id'), "Missing 'template_id' from response: " + res.body);
                    var templateId = parsed.template_id;
                    var instantiatePostRequest = {
                        url: '/api/v1/map/named/' + templateId,
                        method: 'POST',
                        headers: {
                            host: 'localhost',
                            'Content-Type': 'application/json'
                        },
                        data: JSON.stringify(scenario.templateParams)
                    };
                    var next = this;
                    assert.response(
                        server,
                        instantiatePostRequest,
                        {
                            status: 200
                        },
                        function (res, err) {
                            return next(err, res);
                        }
                    );
                },
                function requestTile (err, res) {
                    assert.ifError(err);

                    var layergroupId = JSON.parse(res.body).layergroupid;
                    layergroupIdToDelete = LayergroupToken.parse(layergroupId).token;

                    var torqueTileRequest = {
                        url: '/api/v1/map/' + layergroupId + '/0/0/0/0.torque.json',
                        method: 'GET',
                        headers: {
                            host: 'localhost'
                        }
                    };
                    var next = this;
                    assert.response(
                        server,
                        torqueTileRequest,
                        {
                            status: 200
                        },
                        function (res, err) {
                            return next(err, res);
                        }
                    );
                },
                function validateTileAndFinish (err, res) {
                    if (err) {
                        return done(err);
                    }

                    keysToDelete['map_cfg|' + layergroupIdToDelete] = 0;
                    keysToDelete['map_tpl|localhost'] = 0;
                    keysToDelete['user:localhost:mapviews:global'] = 5;

                    assert.deepStrictEqual(
                        JSON.parse(res.body),
                        scenario.expectedTile
                    );

                    done();
                }
            );
        });
    });
});
